/**
 * @file main.c
 * Betriebsysteme Uebungsbeispiel 1 "myfind"
 * Beispiel 1
 *
 * @author Robert Grasboeck (0910258036) <robert.grasboeck@technikum-wien.at>
 * @author David Schreiber (0910258005) <david.schreiber@technikum-wien.at>
 * @author Nilay Bagchi (0910258057) <nilay.bagchi@technikum-wien.at>
 * @author Tobias Toifl (0910258080) <tobias.toifl@technikum-wien.at>
 * 
 * @date 2010/03/12
 * @version 1.0.0 $Revision: 21 $
 *
 */

/* ======================================================================= */
/*  Header Includes                                                        */
/* ======================================================================= */

#include <stdio.h>        /* printf */
#include <stdlib.h>       /* strtol */
#include <string.h>       /* strlen, strcat, strcpy, strcmp */
#include <sys/stat.h>     /* stat */
#include <dirent.h>       /* dirent, scandir */
#include <time.h>         /* strftime, struct tm */
#include <malloc.h>       /* malloc, free */
#include <unistd.h>       /* readlink */
#include <pwd.h>          /* getpwuid */
#include <grp.h>          /* getgrgid */
#include <fnmatch.h>      /* fnmatch */
#include <ctype.h>        /* isdigit */
#include <assert.h>       /* assert */

/* ======================================================================= */
/*  Defines                                                                */
/* ======================================================================= */

#define SIZE_PERMISSIONSTRING   11 /**< size of string for permissions */
#define SIZE_TIMESTRING         13 /**< size of string for timestamp */
#define SIZE_LINKSTRING        256 /**< size of malloc for readlink */
#define SIZE_UID                32 /**< size of string for uid */
#define SIZE_GID                32 /**< size of string for gid */

/* ======================================================================= */
/*  Typedefs                                                               */
/* ======================================================================= */

/**
 * @brief These are the error codes available. An ErrorCode is constructed
 *        by combining the available errors with a logical or.
 */
typedef enum ErrorCode
{
  EC_NO_ERR,               /**< no error */
  EC_NODIR       = (1<<0), /**< no file or direcotry */
  EC_PERM_DENIED = (1<<1), /**< permission denied */
  EC_WRONG_PARAM = (1<<2)  /**< wrong parameter */
} ErrorCode;

/**
 * @brief These are the different filetypes of a Linux system.       
 */
typedef enum Filetype
{
  TYPE_B,  /**< block (buffered) special */
  TYPE_C,  /**< character (unbuffered) special */
  TYPE_D,  /**< directory */
  TYPE_P,  /**< named pipe (FIFO) */
  TYPE_F,  /**< regular file */
  TYPE_L,  /**< symbolic link; this is never true if the -L option or the -follow option is in effect, 
                unless the symbolic link is broken.  If you want to search for sym‐bolic links when -L is in effect, use -xtype. */
  TYPE_S   /**<  socket */
} Filetype;

/* ======================================================================= */
/*  Globals                                                                */
/* ======================================================================= */

/** 
 * Global variable for storing the name the program is called with
 */
static char * program_name = NULL;

/* ======================================================================= */
/*  Functions                                                              */
/* ======================================================================= */

/**
 * @brief report error
 * @param [in] ec errorcode to report
 * @return error code
 */
static ErrorCode ReportError(ErrorCode ec)
{
  static ErrorCode error;
  
  error |= ec;

  return (error);
}

/**
 * @brief get filetype
 * @param [in] mode mode from lstat
 * @return returns enum Filetype (see Filetype)
 */
static Filetype GetFileType(const int mode)
{
  Filetype filetype = TYPE_F;

  switch (mode & S_IFMT)
  {
    case S_IFIFO:   filetype = TYPE_P; break; /* named pipe (FIFO) */
    case S_IFCHR:   filetype = TYPE_C; break; /* character (unbufferd) special */
    case S_IFDIR:   filetype = TYPE_D; break; /* directory */
    case S_IFBLK:   filetype = TYPE_B; break; /* block (buffered) special */
    case S_IFREG:   filetype = TYPE_F; break; /* regular file */
    case S_IFLNK:   filetype = TYPE_L; break; /* symbolic link */
    case S_IFSOCK:  filetype = TYPE_S; break; /* socket */
    default:        assert(0); break;
  }

  return (filetype);
}

/**
 * @brief returns formated string of given time
 * @param [in] time given time
 * @param [out] str char buffer to write into
 * @return void
 */
static void GetTimeString(const time_t time, char * str)
{
  struct tm brtm = *(localtime(&time));

  strftime(str, SIZE_TIMESTRING, "%b %d %H:%M", &brtm); /* NOTE tryed to used %e instead %d but gcc didn't allowed it !!! */
}

/**
 * @brief returns a readable string for given permissions
 * @param [in] mode mode from lstat to change to a readable string
 * @param [out] str buffer to write into
 * @return filetype
 */
static Filetype GetPermissionString(const int mode, char * str)
{
  char type = '-'; Filetype filetype;

  switch ((filetype = GetFileType(mode)))
  {
    case TYPE_P: type = 'p'; break; /* named pipe (FIFO) */
    case TYPE_C: type = 'c'; break; /* character (unbufferd) special */
    case TYPE_D: type = 'd'; break; /* directory */
    case TYPE_B: type = 'b'; break; /* block (buffered) special */
    case TYPE_F: type = '-'; break; /* regular file */
    case TYPE_L: type = 'l'; break; /* symbolic link */
    case TYPE_S: type = 's'; break; /* socket */
    assert(0); break;
  }

  snprintf(str, SIZE_PERMISSIONSTRING, "%c%c%c%c%c%c%c%c%c%c", /* 10 character + \0 */
                                    type,
                                    (mode & S_IRUSR) ? 'r' : '-',
                                    (mode & S_IWUSR) ? 'w' : '-',
                                    (mode & S_IXUSR) ? 'x' : '-',

                                    (mode & S_IRGRP) ? 'r' : '-',
                                    (mode & S_IWGRP) ? 'w' : '-',
                                    (mode & S_IXGRP) ? 'x' : '-',

                                    (mode & S_IROTH) ? 'r' : '-',
                                    (mode & S_IWOTH) ? 'w' : '-',
            (mode & S_ISVTX) ? 't' :(mode & S_IXOTH) ? 'x' : '-' /* sticky bit */
          );

  return (filetype);
}

/**
 * @brief generates a string from a given symbolic link name
 * @param [in] file_name given file name of link
 * @return address of string
 */
static char * CreateLinkString(const char * file_name)
{
  int size = 0, count = 1, ready = 0;
  char * linkstr = malloc(SIZE_LINKSTRING);

  do
  {
    size = readlink(file_name, linkstr, SIZE_LINKSTRING * count);
    /* printf ("size = %d \n", size); */ /* _DEBUG_ */

    if (size == SIZE_LINKSTRING * count) /* if size is exactly same, realloc more memory */
    {
      count ++;
      linkstr = realloc(linkstr, SIZE_LINKSTRING * count);
    }
    else
    {
      ready = 1;
      linkstr[size] = '\0'; /* terminate with 0 */
    }
  }
  while (!ready);

  return (linkstr);
}

/**
 * @brief print line for -ls argument
 * @param [in] file_name value to change to string
 * @param [in] file_stat filestat result
 * @return void
 */
static void Print_LS(const char * file_name, const struct stat * file_stat)
{
  Filetype filetype;
  char timestr[SIZE_TIMESTRING];        /* 12 + \0 */  
  char permstr[SIZE_PERMISSIONSTRING];  /* 10 + \0 */
  char uid[SIZE_UID]; /* stores string uid in case of missing user */
  char gid[SIZE_GID]; /* stores string gid in case of missing group */

  char * linkstr = NULL;                /* memory for symbolic link */

  snprintf(uid, SIZE_UID, "%d", file_stat->st_uid);
  snprintf(gid, SIZE_GID, "%d", file_stat->st_gid);


  GetTimeString(file_stat->st_mtime, timestr);
  filetype = GetPermissionString(file_stat->st_mode, permstr);

  if (filetype == TYPE_L) linkstr = CreateLinkString(file_name); /* if symbolic link, create string for link info */

  /* *******************************************************************
  inode  blocks  rights    links  user group    sizebyte date      name 
  105996    4 drwxr-xr-x   2 tulan    tulan        4096 Feb 27 14:25 Desktop
    1714    4 -rw-r--r--   1 tulan    tulan         114 Feb 25 21:43 Desktop/DATA
    1901    4 -rw-r--r--   1 tulan    tulan         163 Feb 27 14:25 Desktop/.directory
    1713    4 -rw-r--r--   1 tulan    tulan         117 Feb 25 21:43 Desktop/Windows
  ******************************************************************** */
  /* inode, blocks, permissions, linkcnt, user, group, size, filname, ->, link */
  printf("%6ld %4ld %s %3d %-8s %-8s %8ld %s %s %s%s\n", 
        file_stat->st_ino,
        file_stat->st_blocks,
        permstr,
        (int)file_stat->st_nlink,
        (getpwuid(file_stat->st_uid)) ? getpwuid(file_stat->st_uid)->pw_name : uid,
        (getgrgid(file_stat->st_gid)) ? getgrgid(file_stat->st_gid)->gr_name : gid,
        file_stat->st_size,
        timestr,
        file_name,
        (linkstr) ? "-> " : "",
        (linkstr) ? (linkstr) : "");

  free (linkstr); /* NOTE free is NULL save */
}

/**
 * @brief print line for -print argument
 * @param [in] file_name value to change to string
 * @return void
 */
static void Print_PRINT(const char * file_name)
{
  printf("%s\n", file_name);
}

/**
 * @brief checks if file is from user
 * @param [in] user username to check
 * @param [in] file_stat filestat result
 * @return int success
 * @retval 0 success
 * @retval -1 user not found
 */
static int CheckUser(const char * user, const struct stat * file_stat)
{
  int uid, onlydigits = 1;
  unsigned int i;
  struct passwd * user_info;  
 
  char * username = (char *)user, *end; /* end is end terminator for username */

  /* check if username only exists of digits */
  for (i=0; i<strlen(username) ;i++)
    if (!isdigit(username[i]))
    {
      onlydigits = 0; /* not only digis */
      break;
    }

  /* first try if user name  */
  if ((user_info = getpwnam(username)) != NULL)
  {
    if (user_info->pw_uid == file_stat->st_uid)
      return (0);
  }

  /* if onyl digits, try to calc user id from given string */
  if (onlydigits)
  {
    uid = strtol(username, &end, 10);

    if ((user_info = getpwuid(uid)) != NULL)
    {
      if (user_info->pw_uid == file_stat->st_uid)
        return (0);
    }
  }

  return (-1);
}

/**
 * check for nouser file
 * @param [in] file_stat filestat result
 * @return int result
 * @retval 0 nouser file found
 * @retval -1 no nouser file found
 */
int CheckNoUser(const struct stat * file_stat)
{
  if (getpwuid(file_stat->st_uid) == NULL) return (0);

  return (-1);
}

/**
 * @brief checks if file is from group
 * @param [in] group group to check
 * @param [in] file_stat filestat result
 * @return int success
 * @retval 0 success
 * @retval -1 user not found
 */
static int CheckGroup(const char * group, const struct stat * file_stat)
{
  int gid, onlydigits = 1;
  unsigned int i;
  struct group * group_info;  
 
  char * groupname = (char *)group, *end; /* end is end terminator for group */

  /* check if groupname only exists of digits */
  for (i=0; i<strlen(groupname) ;i++)
    if (!isdigit(groupname[i]))
    {
      onlydigits = 0; /* not only digis */
      break;
    }

  /* first try if user name  */
  if ((group_info = getgrnam(groupname)) != NULL)
  {
    if (group_info->gr_gid == file_stat->st_gid)
      return (0);
  }

  /* if onyl digits, try to calc user id from given string */
  if (onlydigits)
  {
    gid = strtol(groupname, &end, 10);

    if ((group_info = getgrgid(gid)) != NULL)
    {
      if (group_info->gr_gid == file_stat->st_gid)
        return (0);
    }
  }

  return (-1);
}

/**
 * check for nogroup file
 * @param [in] file_stat filestat result
 * @return int result
 * @retval 0 nogroup file found
 * @retval -1 no nogroup file found
 */
int CheckNoGroup(const struct stat * file_stat)
{
  if (getgrgid(file_stat->st_gid) == NULL) return (0);

  return (-1);
}

/**
 * @brief cuts path to filename
 * @param [in] path
 * @return cutted string
 */
static char * GetFileName(const char * path)
{
  char * pathDelimiter = strrchr(path, '/');

  if (pathDelimiter) return (pathDelimiter + 1);

  return (NULL);
}

/**
 * @brief prints filename with given parameters
 * @param [in] file_name given filename
 * @param [in] params given parameters
 * @param [in] file_stat structure from stat system call
 * @return value if file was printed or filterd
 * @retval 0 file was printed
 * @retval -1 file was not printed
 */
static int DoFile(const char * file_name, const char * const * params, const struct stat * file_stat)
{
  int i = 0, def_output = 1;

  while (params[i] != NULL)
  {
    /* -type [bcdpfls] */
    if (strcmp(params[i], "-type") == 0)
    {
      Filetype type;
      switch (params[++i][0])
      {
        case 'b': type = TYPE_B; break;
        case 'c': type = TYPE_C; break;
        case 'd': type = TYPE_D; break;
        case 'p': type = TYPE_P; break;
        case 'f': type = TYPE_F; break;
        case 'l': type = TYPE_L; break;
        case 's': type = TYPE_S; break;
        assert(0); break;
      }
      if (GetFileType(file_stat->st_mode) != type) return (-1);
    }
    /* -name <pattern> */
    else if (strcmp(params[i], "-name") == 0)
    {
      char *filename = GetFileName(file_name);
      if (!filename) return (-1);
      if (FNM_NOMATCH == fnmatch(params[++i], filename, 0)) return (-1);
    }
    /* -path <pattern> */
    else if (strcmp(params[i], "-path") == 0)
    {
      if (FNM_NOMATCH == fnmatch(params[++i], file_name, 0)) return (-1);
    }
    /* -nouser */
    else if (strcmp(params[i], "-nouser") == 0)
    {
      if (CheckNoUser(file_stat)) return (-1);
    }
    /* -user <uid>/<username> */
    else if (strcmp(params[i], "-user") == 0)
    {
      if (CheckUser(params[++i], file_stat)) return (-1);
    }
    /* -nogroup */
    else if (strcmp(params[i], "-nogroup") == 0)
    {
      if (CheckNoGroup(file_stat)) return (-1);
    }
    /* -group <gid>/<groupname> */
    else if (strcmp(params[i], "-group") == 0)
    {
      if (CheckGroup(params[++i], file_stat)) return (-1);
    }
    /* -ls */
    else if (strcmp(params[i], "-ls") == 0)
    {
      Print_LS(file_name, file_stat);
      def_output = 0;
    }
    /* -print */
    else if (strcmp(params[i], "-print") == 0)
    {
      Print_PRINT(file_name);
      def_output = 0;
    }

    i++; /* next parameter */
  }

  /* if no output parameter was set, use print as default */
  if (def_output) 
    Print_PRINT(file_name);

  return (0);
}

/**
 * @brief creates a new filename (malloc) from name + d_name
 * @param [in] name current name
 * @param [in] directory directory name to add
 * @return char * newname
 * @note filename memory os allocated by malloc, don't forget to free!
 */
static char * CreateNewname(const char * name, const char * directory)
{
  int size = strlen(name) + strlen(directory) + 2; /* name + d_name + / + \0 */
  char * newname = malloc(size);

  if (newname)
  {
    strcpy(newname, name);

    /* remove extra '/' */
    if (newname[strlen(newname)-1] == '/')
      newname[strlen(newname)-1] = '\0';

    strcat(newname, "/");
    strcat(newname, directory);
  }

  return (newname); /* NOTE maybe malloc failed, but usually not !!! */
}

/**
 * @brief filter for scandir function
 * @param [in] entry current entry to filter
 * @return int
 * @retval 0 don't add to list
 * @retval 1 add to list
 */
static int Filter(const struct dirent *entry)
{
  if (strcmp(entry->d_name, "..") == 0) return (0);
  if (strcmp(entry->d_name, ".") == 0)  return (0);

  return (1);
}

/**
 * @brief recursive list function
 * @param [in] name start entry (file or directory)
 * @param [in] params given params
 * @return int
 * @retval 0 succesfull
 * @retval 1 error
 */
static void List(const char * name, const char * const * params)
{
  struct stat file_stat; /* check for file */

  /* lstat did fail? */
  if (lstat(name, &file_stat) < 0) 
  { 
    perror(program_name);
    ReportError(EC_NODIR);
    return;
  }

  /***********************/
  /* print infos         */
  /***********************/
  DoFile(name, params, &file_stat); /* print infos */

  /***********************/
  /* is it a directory ? */
  /***********************/
  if (S_ISDIR(file_stat.st_mode)) 
  {
    int n; struct dirent **direntrylist; /* for listing of directory */

    n = scandir(name, &direntrylist, Filter, NULL /* could be alphasort ;) */);

    if (n >= 0)
    {
      int cnt;
      for (cnt=0; cnt<n; cnt++)
      {
        char * newname = CreateNewname(name, direntrylist[cnt]->d_name);
        List(newname, params);
        free (newname); /* free memory for name */

        free(direntrylist[cnt]); /* because scandir made a list with malloc !!! */
      }
      free(direntrylist); /* because scandir made a list with malloc !!! */
    }
    else
    {
      perror(program_name); /* TODO add filename !!!*/
      ReportError(EC_PERM_DENIED);
    }
  }
}

/**
 * @brief print usage help
 */
static void PrintUsage(void)
{
  printf("Usage: %s [path...] [expression]:\n", program_name);
  printf("\t-user <name</<uid>\n");
  printf("\t-nouser\n");
  printf("\t-name <pattern>\n");
  printf("\t-path <pattern>\n");
  printf("\t-type [bcdpfls]\n");
  printf("\t-print\n");
  printf("\t-ls\n");
  printf("\t-group  <name>/<gid>\n");
  printf("\t-nogroup\n");
}

/**
 * @brief The most minimalistic C program
 *        This is the main entry point for any C program.
 *
 * @param [in] argc the number of arguments given
 * @param [in] argv the arguments itselves (including the program name in argv[0])
 *
 * @return int The error code produced
 * @retval 0-MaxErrorValue
 */
int main(int argc, const char *argv[])
{
  /* Used for iterating through all command line arguments */
  /* Starting with 1 (0... program name) */
  int currentArgument = 1;

  /* Used to determine the correct argument order */
  /* This variable is 0 until the first output parameter is found while iterating */
  int option = 0;

  /* These are markers for the position of the first searchpath argument and the last */
  int start_searchpath = -1;
  int end_searchpath   = -1;
  int i;

  /* Store the program name (in case the executable is renamed) */
  program_name = (char*)argv[0];

  /* Begin parsing of arguments & check for correctness */
  while (currentArgument < argc)
  {
    /* Output the help & usage information & exit */
    if (strcmp(argv[currentArgument], "--help") == 0)
    {
      PrintUsage();
      return (EC_NO_ERR);
    }
    /* -ls */
    else if (strcmp(argv[currentArgument], "-ls") == 0)
      option = 1;
    /* -print  */
    else if (strcmp(argv[currentArgument], "-print") == 0)
      option = 1;
    /* -name <pattern> */
    else if (strcmp(argv[currentArgument], "-name") == 0)
    {
      option = 1;
      if (argc > currentArgument+1)
        currentArgument ++;
      else
      {
        fprintf(stderr, "missing argument for \"-name\".\n");
        return (EC_WRONG_PARAM);
      }
    }
    /* -patch <pattern> */
    else if (strcmp(argv[currentArgument], "-path") == 0)
    {
      option = 1;
      if (argc > currentArgument+1)
        currentArgument ++;
      else
      {
        fprintf(stderr, "missing argument for \"-path\"\n");
        return (EC_WRONG_PARAM);
      }
    }
    /* -user <name>/<uid> */
    else if (strcmp(argv[currentArgument], "-user") == 0)
    {
      option = 1;
      if (argc > currentArgument+1)
        currentArgument ++;
      else
      {
        fprintf(stderr, "missing argument for \"-user\".\n");
        return (EC_WRONG_PARAM);
       }
    }
    /* -nouser */
    else if (strcmp(argv[currentArgument], "-nouser") == 0)
      option = 1;
    /* -type [bcdpfls] */
    else if (strcmp(argv[currentArgument], "-type") == 0)
    {
      option = 1;
      if (argc > currentArgument+1)
      {
        currentArgument ++;

        if (strlen(argv[currentArgument]) > 1)
        {
          fprintf(stderr, "the argument for \"-type\" must be a single letter.\n");
          return (EC_WRONG_PARAM);
        }

        switch (argv[currentArgument][0])
        {
          case 'b': case 'c':  case 'd':  case'p':  case'f':  case'l':  case 's':
            break;
          default:
            fprintf(stderr, "%s: invalid argument '%s' to '-type'.\n", program_name, argv[currentArgument]);
            return (EC_WRONG_PARAM);
        }
      }
      else
      {
        fprintf(stderr, "missing argument for \"-user\".\n");
        return (EC_WRONG_PARAM);
      }
    }
    else
    {
      if (option)
      {
        fprintf(stderr, "%s: paths must precede expression.\n", program_name);
        return (EC_WRONG_PARAM);
      }
      else
      {
        if (start_searchpath < 0) start_searchpath = currentArgument;
        end_searchpath = currentArgument;
      }
    }

    currentArgument ++;
  }


  /* No path argument given -> Search current directory */
  if (start_searchpath < 0 && end_searchpath < 0)
     List(".", argv);
  else
  {
    /* Iterate through all given searchpaths and execute file listing */
    for (i=start_searchpath; i<=end_searchpath; i++)
      List(argv[i], argv);
  }

  /* Exit returning the appropriate error code */
  return (ReportError(EC_NO_ERR));
}
